package za.ac.sun.cs.distrib.logging;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Date;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apfloat.Apint;

public class LogCollector {

	private static final int DEFAULT_LOG_PORT = 7711;

	private static final int DEFAULT_HTTP_PORT = 7722;
	
	private static final String DEFAULT_LOG_FILENAME = "output.txt";

	public static void main(String[] args) {
		int p = DEFAULT_LOG_PORT;
		int hp = DEFAULT_HTTP_PORT;
		String f = DEFAULT_LOG_FILENAME;
		int i = 0;
		while (i < args.length) {
			String o = args[i++];
			if (o.equals("--port")) {
				if (i < args.length) {
					try {
						p = Integer.parseInt(args[i++]);
					} catch (NumberFormatException x) {
						System.out.println("Illegal port number ignored");
					}
				} else {
					System.out.println("Missing port number");
				}
			} else if (o.startsWith("--port=")) {
				try {
					p = Integer.parseInt(o.substring(7));
				} catch (NumberFormatException x) {
					System.out.println("Illegal port number ignored");
				}
			} else if (o.equals("--httpport")) {
				if (i < args.length) {
					try {
						hp = Integer.parseInt(args[i++]);
					} catch (NumberFormatException x) {
						System.out.println("Illegal http port number ignored");
					}
				} else {
					System.out.println("Missing http port number");
				}
			} else if (o.startsWith("--httpport=")) {
				try {
					hp = Integer.parseInt(o.substring(11));
				} catch (NumberFormatException x) {
					System.out.println("Illegal http port number ignored");
				}
			} else if (o.equals("--file")) {
				if (i < args.length) {
					f = args[i++];
				} else {
					System.out.println("Missing filename");
				}
			} else if (o.startsWith("--file=")) {
				f = o.substring(7);
			} else {
				System.out.println("Unrecognized option \"" + o + "\"");
			}
		}
		new LogCollector(p, hp, f).run();
	}

	private int logPort;

	private int httpPort;
	
	private String logFilename;

	private LogFile logFile;
	
	private ServerSocket logServerSocket;

	private ServerSocket httpServerSocket;

	private int activeClients;
	
	private boolean running;

	private Apint totalBranchValues = Apint.ZERO;

	private final Set<String> firstBranch = new TreeSet<String>();

	private final SortedSet<ClientInfo> clients = new TreeSet<LogCollector.ClientInfo>();

	public LogCollector(int logPort, int httpPort, String logFilename) {
		this.logPort = logPort;
		this.httpPort = httpPort;
		this.logFilename = logFilename;
		logFile = null;
		activeClients = 0;
		running = true;
	}

	public void run() {
		// Start the HTTP server in another thread
		new Thread(new HttpServer()).start();

		System.out.println("Collector started");

		// Creating output file
		try {
			logFile = new LogFile(logFilename);
		} catch (IOException x) {
			x.printStackTrace();
			return;
		}

		// Creating socket
		try {
			logServerSocket = new ServerSocket(logPort);
		} catch (IOException x) {
			x.printStackTrace();
			return;
		}

		// Waiting for clients to connect
		try {
			while (running) {
				new Thread(new Handler(logServerSocket.accept(), logFile)).start();
			}
		} catch (SocketException x) {
			// this is normal!
		} catch (IOException x) {
			x.printStackTrace();
			return;
		}

		// Closing file
		logFile.closeFile();
	}

	public void terminate() {
		running = false;
		try {
			logServerSocket.close();
			// httpServerSocket.close();
		} catch (IOException x) {
			x.printStackTrace();
		}
		for (ClientInfo ci : clients) {
			ci.setActive(false);
		}
		new Thread(new HttpKiller()).start();
	}

	private synchronized void addClient(ClientInfo clientInfo) {
		clients.add(clientInfo);
		activeClients++;
	}

	private synchronized void removeClient(ClientInfo clientInfo) {
		clientInfo.setActive(false);
		activeClients--;
		if (activeClients == 0) {
			terminate();
		}
	}

	private class Handler implements Runnable {

		private final Socket socket;

		private final LogFile logFile;

		private BufferedReader input;

		public Handler(Socket socket, LogFile logFile) {
			this.socket = socket;
			this.logFile = logFile;
		}

		@Override
		public void run() {
			try {
				InputStreamReader r = new InputStreamReader(
						socket.getInputStream());
				input = new BufferedReader(r);
			} catch (IOException x) {
				x.printStackTrace();
				return;
			}

			InetAddress ip = socket.getInetAddress();
			ClientInfo clientInfo = new ClientInfo(socket.toString(), ip);
			addClient(clientInfo);
			try {
				String line = input.readLine();
				while (line != null) {
					logFile.log(line);
					clientInfo.incLogEntries();
					if (line.contains("@@ DONE @@")) {
						break;
					} else if (line.contains("@@ ID @@")) {
						int id = Integer.parseInt(line.split(" @@ ")[2].trim());
						clientInfo.setID(id);
					} else if (line.contains("@@ CHOICE @@")) {
						clientInfo.incChoiceCount();
					} else if (line.contains("@@ CONFLICT @@")) {
						clientInfo.incConflictCount();
					} else if (line.contains("@@ UPDATE @@ root")) {
						Long cpuTime = Long.parseLong(line.split(" @@ ")[3].trim());
						clientInfo.setCpuTime(cpuTime);
						clientInfo.incPathCount();
						clientInfo.incUpdateCount();
					} else if (line.contains("@@ UPDATE @@") && !line.contains("&&")) {
						Apint delta = new Apint(line.split(" @@ ")[4].trim());
						clientInfo.incValueCount(delta);
						clientInfo.incUpdateCount();
					} else if (line.contains("@@ UPDATE @@")) {
						clientInfo.incUpdateCount();
					} else if (line.contains("@@ NEWCOUNT @@") && !line.contains("&&")) {
						String[] fields = line.split(" @@ ");
						String pc = fields[2].trim();
						if (!firstBranch.contains(pc)) {
							firstBranch.add(pc);
							totalBranchValues = totalBranchValues.add(new Apint(fields[3].trim()));
						}
					} else if (line.contains("@@ NOCHOICE @@")) {
						clientInfo.incDeadEnds();
					}
					line = input.readLine();
				}
			} catch (IOException x) {
				x.printStackTrace();
			}
			removeClient(clientInfo);

			try {
				socket.close();
			} catch (IOException x) {
				x.printStackTrace();
				return;
			}
		}

	}

	private class LogFile {

		private final BufferedWriter output;

		public LogFile(String filename) throws IOException {
			File f = new File(filename);
			FileWriter w = new FileWriter(f);
			output = new BufferedWriter(w);
		}

		private synchronized void log(String message) {
			try {
				StringBuilder b = new StringBuilder();
				b.append("@@ ").append(System.currentTimeMillis());
				b.append(' ').append(message).append('\n');
				output.write(b.toString());
			} catch (IOException x) {
				x.printStackTrace();
			}
		}

		private synchronized void closeFile() {
			try {
				output.close();
			} catch (IOException x) {
				x.printStackTrace();
			}
		}

	}

	private class HttpKiller implements Runnable {

		@Override
		public void run() {
			System.out.println("HTTP server has 15 second to live");
			try {
				Thread.sleep(15000);
			} catch (InterruptedException x) {
				x.printStackTrace();
			}
			try {
				httpServerSocket.close();
			} catch (IOException x) {
				x.printStackTrace();
				System.exit(1);
			}
		}

	}
	
	private class HttpServer implements Runnable {
		
		private final String UPDATING_HEADER = new StringBuilder()
				.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\r\n")
				.append("<html><head>\r\n")
				.append("<title>LogCollector</title>\r\n")
				.append("<style>*{font-family:Arial,\"Helvetica Neue\",Helvetica,sans-serif;font-size:10pt;}</style>\r\n")
				.append("<meta http-equiv=refresh content='5'>\r\n")
				.append("</head><body>\r\n").toString();

		private final String NONUPDATING_HEADER = new StringBuilder()
				.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\r\n")
				.append("<html><head>\r\n")
				.append("<title>LogCollector</title>\r\n")
				.append("<style>*{font-family:Arial,\"Helvetica Neue\",Helvetica,sans-serif;font-size:10pt;}</style>\r\n")
				.append("</head><body>\r\n").toString();

		private final String TABLE_HEADER = new StringBuilder()
				.append("<table border=\"1\"><tr>\r\n")
				.append("<th>&nbsp;&nbsp;IP&nbsp;&nbsp;</th>\r\n")
				.append("<th>&nbsp;&nbsp;ID&nbsp;&nbsp;</th>\r\n")
				.append("<th>Active?</th>\r\n").append("<th>Entries</th>\r\n")
				.append("<th>Paths</th>\r\n").append("<th>Values</th>\r\n")
				.append("<th>Overlaps</th>\r\n").append("<th>Choices</th>\r\n")
				.append("<th>Updates</th>\r\n").append("<th>Deadends</th>\r\n")
				.append("<th>CPU time</th>\r\n")
				.toString();

		@Override
		public void run() {
			System.out.println("HTTP server started");
			try {
				httpServerSocket = new ServerSocket(httpPort);
			} catch (IOException x) {
				x.printStackTrace();
				System.exit(1);
			}

			try {
				while (true) {
					Socket socket = httpServerSocket.accept();
					InputStreamReader r = new InputStreamReader(
							socket.getInputStream());
					BufferedReader input = new BufferedReader(r);
					String line = input.readLine();
					if (line == null) {
						System.out.println("HTTP request is null -- stopping the server");
						// break;
					}
					String[] parts = line.split(" ");
					DataOutputStream output = new DataOutputStream(socket.getOutputStream());
					if ((parts.length == 3) && (parts[0].equals("GET"))) {
						if (parts[1].equals("/")) {
							output.writeBytes("HTTP/1.0 200 OK\r\n");
							output.writeBytes("Server: LogCollector\r\n");
							output.writeBytes("Content-type: text/html; charset=utf-8\r\n");
							String header = running ? UPDATING_HEADER : NONUPDATING_HEADER;
							StringBuilder b = new StringBuilder().append(header);
							b.append("<p>").append(new Date().toString());
							if (!running) {
								b.append("&nbsp;Done");
							}
							b.append("</p>\r\n");
							b.append(TABLE_HEADER);
							int totalLogEntries = 0;
							int totalPaths = 0;
							Apint totalValues = Apint.ZERO;
							int totalConflicts = 0;
							int totalChoices = 0;
							int totalUpdates = 0;
							int totalDeadEnds = 0;
							for (ClientInfo ci : clients) {
								if (ci.isActive()) {
									b.append("</tr><tr bgcolor=\"#99ff99\">\r\n");
								} else {
									b.append("</tr><tr bgcolor=\"#ff9999\">\r\n");
								}
								String cpuTime = String.format("%.2f", ci.getCpuTime() * 0.000000001);
								b.append("<td>").append(ci.getIP()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getID()).append("</td>\r\n");
								b.append("<td align=\"center\">").append(ci.isActive() ? "yes" : "no").append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getLogEntries()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getPathCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getValueCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getConflictCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getChoiceCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getUpdateCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(ci.getDeadEndCount()).append("</td>\r\n");
								b.append("<td align=\"right\">").append(cpuTime).append("</td>\r\n");
								totalLogEntries += ci.getLogEntries();
								totalPaths += ci.getPathCount();
								totalValues = totalValues.add(ci.getValueCount());
								totalConflicts += ci.getConflictCount();
								totalChoices += ci.getChoiceCount();
								totalUpdates += ci.getUpdateCount();
								totalDeadEnds += ci.getDeadEndCount();
							}
							b.append("</tr><tr>\r\n");
							b.append("<th colspan=\"3\" align=\"right\" valign=\"top\">TOTAL</td>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalLogEntries).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalPaths).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalValues);
							b.append("<br/>").append(totalBranchValues).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalConflicts).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalChoices).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalUpdates).append("</th>\r\n");
							b.append("<th align=\"right\" valign=\"top\">").append(totalDeadEnds).append("</th>\r\n");
							b.append("<th>&nbsp;</th>\r\n");
							b.append("</tr></table>\r\n");
							b.append("</body></html>\r\n");
							output.writeBytes("Content-Length: " + b.length() + 4 + "\r\n");
							output.writeBytes("\r\n");
							output.writeBytes(b.toString());
							output.writeBytes("\r\n");
						} else {
						}
					}
					socket.close();
				}
			} catch (SocketException x) {
				// this is normal!
			} catch (IOException x) {
				x.printStackTrace();
				return;
			}

			System.out.println("HTTP server stopping");
			try {
				httpServerSocket.close();
			} catch (IOException x) {
				x.printStackTrace();
				return;
			}
		}

	}

	private class ClientInfo implements Comparable<ClientInfo> {

		private final String socket;

		private final String ip;
		
		private int id;

		private boolean isActive;

		private int logEntries;

		private int pathCount;
		
		private long cpuTime;
		
		private Apint valueCount;

		private int conflictCount;

		private int choiceCount;

		private int updateCount;
		
		private int deadEndCount;
		
		public ClientInfo(String socket, InetAddress ip) {
			this.socket = socket;
			this.ip = ip.toString();
			id = -1;
			isActive = true;
			logEntries = 0;
			pathCount = 0;
			valueCount = Apint.ZERO;
			conflictCount = 0;
			choiceCount = 0;
			updateCount = 0;
			deadEndCount = 0;
		}
		
		public int getID() {
			return id;
		}
		
		public void setID(int id) {
			this.id = id;
		}
		
		public String getIP() {
			return ip;
		}
		
		public void setActive(boolean isActive) {
			this.isActive = isActive;
		}
		
		public boolean isActive() {
			return isActive;
		}
		
		public void incLogEntries() {
			logEntries++;
		}

		public int getLogEntries() {
			return logEntries;
		}

		public void setCpuTime(long cpuTime) {
			this.cpuTime = cpuTime;
		}
		
		public long getCpuTime() {
			return cpuTime;
		}
		
		public void incValueCount(Apint delta) {
			valueCount = valueCount.add(delta);
		}
		
		public Apint getValueCount() {
			return valueCount;
		}
		
		public void incPathCount() {
			pathCount++;
		}
		
		public int getPathCount() {
			return pathCount;
		}
		
		public void incConflictCount() {
			conflictCount++;
		}
		
		public int getConflictCount() {
			return conflictCount;
		}
		
		public void incChoiceCount() {
			choiceCount++;
		}
		
		public int getChoiceCount() {
			return choiceCount;
		}
		
		public void incUpdateCount() {
			updateCount++;
		}
		
		public int getUpdateCount() {
			return updateCount;
		}
		
		public void incDeadEnds() {
			deadEndCount++;
		}
		
		public int getDeadEndCount() {
			return deadEndCount;
		}
		
		@Override
		public int compareTo(ClientInfo clientInfo) {
			return socket.compareTo(clientInfo.socket);
		}

	}
		
}
